/**
 * This file is part of aion-emu <aion-emu.com>.
 *
 *  aion-emu is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-emu is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-emu.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionengine.gameserver.world;

import com.aionengine.gameserver.configs.main.WorldConfig;
import com.aionengine.gameserver.instance.handlers.InstanceHandler;
import com.aionengine.gameserver.model.gameobjects.AionObject;
import com.aionengine.gameserver.model.gameobjects.Npc;
import com.aionengine.gameserver.model.gameobjects.StaticDoor;
import com.aionengine.gameserver.model.gameobjects.VisibleObject;
import com.aionengine.gameserver.model.gameobjects.player.Player;
import com.aionengine.gameserver.model.team2.alliance.PlayerAlliance;
import com.aionengine.gameserver.model.team2.group.PlayerGroup;
import com.aionengine.gameserver.model.team2.league.League;
import com.aionengine.gameserver.model.templates.quest.QuestNpc;
import com.aionengine.gameserver.model.templates.world.WorldMapTemplate;
import com.aionengine.gameserver.model.templates.zone.ZoneClassName;
import com.aionengine.gameserver.model.templates.zone.ZoneType;
import com.aionengine.gameserver.questEngine.QuestEngine;
import com.aionengine.gameserver.world.exceptions.DuplicateAionObjectException;
import com.aionengine.gameserver.world.knownlist.Visitor;
import com.aionengine.gameserver.world.zone.RegionZone;
import com.aionengine.gameserver.world.zone.ZoneInstance;
import com.aionengine.gameserver.world.zone.ZoneName;
import com.aionengine.gameserver.world.zone.ZoneService;
import com.aionengine.gameserver.model.Race;

import gnu.trove.map.hash.TIntObjectHashMap;
import javolution.util.FastMap;
import javolution.util.FastSet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.Future;

/**
 * World map instance object.
 *
 * @author -Nemesiss-
 */
public abstract class WorldMapInstance {

    /**
     * Logger for this class.
     */
    private static final Logger log = LoggerFactory.getLogger(WorldMapInstance.class);
    /**
     * Size of region
     */
    public static final int regionSize = WorldConfig.WORLD_REGION_SIZE;
    /**
     * WorldMap witch is parent of this instance.
     */
    private final WorldMap parent;
    /**
     * Map of active regions.
     */
    protected final TIntObjectHashMap<MapRegion> regions = new TIntObjectHashMap<MapRegion>();

    /**
     * All objects spawned in this world map instance
     */
    private final Map<Integer, VisibleObject> worldMapObjects = new FastMap<Integer, VisibleObject>().shared();

    /**
     * All players spawned in this world map instance
     */
    private final FastMap<Integer, Player> worldMapPlayers = new FastMap<Integer, Player>().shared();

    private final Set<Integer> registeredObjects = Collections.newSetFromMap(new FastMap<Integer, Boolean>().shared());

    private PlayerGroup registeredGroup = null;

    private Future<?> emptyInstanceTask = null;

    /**
     * Id of this instance (channel)
     */
    private int instanceId;

    private final FastSet<Integer> questIds = new FastSet<Integer>();

    private InstanceHandler instanceHandler;

    private Map<ZoneName, ZoneInstance> zones = new HashMap<ZoneName, ZoneInstance>();

    // TODO: Merge this with owner
    private Integer soloPlayer;

    private PlayerAlliance registredAlliance;
    private League registredLeague;
    
    InstanceType instanceType = InstanceType.NORMAL;

    /**
     * Constructor.
     *
     * @param parent
     */
    public WorldMapInstance(WorldMap parent, int instanceId) {
        this.parent = parent;
        this.instanceId = instanceId;
        this.zones = ZoneService.getInstance().getZoneInstancesByWorldId(parent.getMapId());
        initMapRegions();
    }

    /**
     * Return World map id.
     *
     * @return world map id
     */
    public Integer getMapId() {
        return getParent().getMapId();
    }

    /**
     * Returns WorldMap witch is parent of this instance
     *
     * @return parent
     */
    public WorldMap getParent() {
        return parent;
    }

    public WorldMapTemplate getTemplate() {
        return parent.getTemplate();
    }

    /**
     * Returns MapRegion that contains coordinates of VisibleObject. If the region doesn't exist, it's created.
     *
     * @param object
     * @return a MapRegion
     */
    MapRegion getRegion(VisibleObject object) {
        return getRegion(object.getX(), object.getY(), object.getZ());
    }

    /**
     * Returns MapRegion that contains given x,y coordinates. If the region doesn't exist, it's created.
     *
     * @param x
     * @param y
     * @return a MapRegion
     */
    public abstract MapRegion getRegion(float x, float y, float z);

    /**
     * Create new MapRegion and add link to neighbours.
     *
     * @param regionId
     * @return newly created map region
     */
    protected abstract MapRegion createMapRegion(int regionId);

    protected abstract void initMapRegions();

    public abstract boolean isPersonal();

    public abstract int getOwnerId();

    /**
     * Returs {@link World} instance to which belongs this WorldMapInstance
     *
     * @return World
     */
    public World getWorld() {
        return getParent().getWorld();
    }

    /**
     * @param object
     */
    public void addObject(VisibleObject object) {
        if (worldMapObjects.put(object.getObjectId(), object) != null) {
            throw new DuplicateAionObjectException("Object with templateId " + String.valueOf(object.getObjectTemplate().getTemplateId())
                    + " already spawned in the instance " + String.valueOf(this.getMapId()) + " " + String.valueOf(this.getInstanceId()));
        }
        if (object instanceof Npc) {
            int npcId = ((Npc) object).getNpcId();
            QuestNpc data = QuestEngine.getInstance().getQuestNpc(npcId);
            if (data != null) {
                HashSet<Integer> questsToCheck = data.getNotCheckedQuestIds();
                if (!questsToCheck.isEmpty()) {
                    for (int id : questsToCheck) {
                        QuestEngine.getInstance().onNotifyNpcSpawned(id, npcId);
                    }
                    data.setWasSpawned(true);
                }
                for (int id : data.getOnQuestStart()) {
                    if (!questIds.contains(id))
                        questIds.add(id);
                }
            }
        }
        if (object instanceof Player) {
            if (this.getParent().isPossibleFly())
                ((Player) object).setInsideZoneType(ZoneType.FLY);
            worldMapPlayers.put(object.getObjectId(), (Player) object);
        }
    }

    /**
     * @param object
     */
    public void removeObject(AionObject object) {
        worldMapObjects.remove(object.getObjectId());
        if (object instanceof Player) {
            if (this.getParent().isPossibleFly())
                ((Player) object).unsetInsideZoneType(ZoneType.FLY);
            worldMapPlayers.remove(object.getObjectId());
        }
    }

    /**
     * @param npcId
     * @return npc
     */
    public Npc getNpc(int npcId) {
        for (Iterator<VisibleObject> iter = objectIterator(); iter.hasNext(); ) {
            VisibleObject obj = iter.next();
            if (obj instanceof Npc) {
                Npc npc = (Npc) obj;
                if (npc.getNpcId() == npcId) {
                    return npc;
                }
            }
        }
        return null;
    }

    public List<Player> getPlayersInside() {
        List<Player> playersInside = new ArrayList<Player>();
        Iterator<Player> players = playerIterator();
        while (players.hasNext()) {
            playersInside.add(players.next());
        }
        return playersInside;
    }
    
    public List<Player> getAsmoPlayersInside() {
		List<Player> playersInside = new ArrayList<Player>();
		Iterator<Player> players = playerIterator();
		while (players.hasNext()) {
			Player nextPlayer = players.next();
			if (nextPlayer.getRace() == Race.ASMODIANS)
				playersInside.add(nextPlayer);
		}
		return playersInside;
	}
    
    public List<Player> getElyPlayersInside() {
		List<Player> playersInside = new ArrayList<Player>();
		Iterator<Player> players = playerIterator();
		while (players.hasNext()) {
			Player nextPlayer = players.next();
			if (nextPlayer.getRace() == Race.ELYOS)
				playersInside.add(nextPlayer);
		}
		return playersInside;
	}

    /**
     * @param npcId
     * @return List<npc>
     */
    public List<Npc> getNpcs(int npcId) {
        List<Npc> npcs = new ArrayList<Npc>();
        for (Iterator<VisibleObject> iter = objectIterator(); iter.hasNext(); ) {
            VisibleObject obj = iter.next();
            if (obj instanceof Npc) {
                Npc npc = (Npc) obj;
                if (npc.getNpcId() == npcId) {
                    npcs.add(npc);
                }
            }
        }
        return npcs;
    }

    /**
     * @return List<npcs>
     */
    public List<Npc> getNpcs() {
        List<Npc> npcs = new ArrayList<Npc>();
        for (Iterator<VisibleObject> iter = objectIterator(); iter.hasNext(); ) {
            VisibleObject obj = iter.next();
            if (obj instanceof Npc) {
                npcs.add((Npc) obj);
            }
        }
        return npcs;
    }

    /**
     * @return List<doors>
     */
    public Map<Integer, StaticDoor> getDoors() {
        Map<Integer, StaticDoor> doors = new HashMap<Integer, StaticDoor>();
        for (Iterator<VisibleObject> iter = objectIterator(); iter.hasNext(); ) {
            VisibleObject obj = iter.next();
            if (obj instanceof StaticDoor) {
                StaticDoor door = (StaticDoor) obj;
                doors.put(door.getSpawn().getStaticId(), door);
            }
        }
        return doors;
    }

    /**
     * @return the instanceIndex
     */
    public int getInstanceId() {
        return instanceId;
    }

    public final boolean isBeginnerInstance() {
        if (parent == null)
            return false;

        if (parent.getTemplate().isInstance()) {
            // TODO: check Karamatis and Ataxiar for exception in FastTrack ?
            // return parent.getTemplate().getBeginnerTwinCount() > 0;
            return false;
        }

        int twinCount = parent.getTemplate().getTwinCount();
        if (twinCount == 0)
            twinCount = 1;
        return getInstanceId() > twinCount;
    }

    /**
     * Check player is in instance
     *
     * @param objId
     * @return
     */
    public boolean isInInstance(int objId) {
        return worldMapPlayers.containsKey(objId);
    }

    /**
     * @return
     */
    public Iterator<VisibleObject> objectIterator() {
        return worldMapObjects.values().iterator();
    }

    /**
     * @return
     */
    public Iterator<Player> playerIterator() {
        return worldMapPlayers.values().iterator();
    }

    public void registerGroup(PlayerGroup group) {
        registeredGroup = group;
        register(group.getTeamId());
    }

    public void registerGroup(PlayerAlliance group) {
        registredAlliance = group;
        register(group.getObjectId());
    }

    public void registerGroup(League group) {
        registredLeague = group;
        register(group.getObjectId());
    }

    public PlayerAlliance getRegistredAlliance() {
        return registredAlliance;
    }

    public League getRegistredLeague() {
        return registredLeague;
    }

    /**
     * @param objectId
     */
    public void register(int objectId) {
        registeredObjects.add(objectId);
    }

    /**
     * @param objectId
     * @return
     */
    public boolean isRegistered(int objectId) {
        return registeredObjects.contains(objectId);
    }

    /**
     * @return the emptyInstanceTask
     */
    public Future<?> getEmptyInstanceTask() {
        return emptyInstanceTask;
    }

    /**
     * @param emptyInstanceTask the emptyInstanceTask to set
     */
    public void setEmptyInstanceTask(Future<?> emptyInstanceTask) {
        this.emptyInstanceTask = emptyInstanceTask;
    }

    /**
     * @return the registeredGroup
     */
    public PlayerGroup getRegisteredGroup() {
        return registeredGroup;
    }

    /**
     * @return
     */
    public int playersCount() {
        return worldMapPlayers.size();
    }

    public FastSet<Integer> getQuestIds() {
        return questIds;
    }

    public final InstanceHandler getInstanceHandler() {
        return instanceHandler;
    }

    public final void setInstanceHandler(InstanceHandler instanceHandler) {
        this.instanceHandler = instanceHandler;
    }

    public Player getPlayer(Integer object) {
        for (Player player : worldMapPlayers.values()) {
            if (object == player.getObjectId()) {
                return player;
            }
        }
        return null;
    }

    /**
     * @param visitor
     */
    public void doOnAllPlayers(Visitor<Player> visitor) {
        try {
            for (Player player : worldMapPlayers.values())
                if (player != null) {
                    visitor.visit(player);
                }
        } catch (Exception ex) {
            log.error("Exception when running visitor on all players" + ex);
        }
    }

    protected ZoneInstance[] filterZones(int mapId, int regionId, float startX, float startY, float minZ, float maxZ) {
        List<ZoneInstance> regionZones = new ArrayList<ZoneInstance>();
        RegionZone regionZone = new RegionZone(startX, startY, minZ, maxZ);

        for (ZoneInstance zoneInstance : zones.values()) {
            if (zoneInstance.getAreaTemplate().intersectsRectangle(regionZone))
                regionZones.add(zoneInstance);
            else if (zoneInstance.getZoneTemplate().getZoneType() == ZoneClassName.DUMMY) {
                log.error("Region " + regionId + " should intersect with whole map zone!!! (map=" + mapId + ")");
            }
        }
        return regionZones.toArray(new ZoneInstance[regionZones.size()]);
    }

    /**
     * @param player
     * @param zoneName
     * @return
     */
    public boolean isInsideZone(VisibleObject object, ZoneName zoneName) {
        ZoneInstance zoneTemplate = zones.get(zoneName);
        if (zoneTemplate == null)
            return false;
        return isInsideZone(object.getPosition(), zoneName);
    }

    /**
     * @param pos
     * @param zone
     * @return
     */
    public boolean isInsideZone(WorldPosition pos, ZoneName zoneName) {
        MapRegion mapRegion = this.getRegion(pos.getX(), pos.getY(), pos.getZ());
        return mapRegion.isInsideZone(zoneName, pos.getX(), pos.getY(), pos.getZ());
    }

    public void setSoloPlayerObj(Integer obj) {
        soloPlayer = obj;
    }

    public Integer getSoloPlayerObj() {
        return soloPlayer;
    }
    
    public InstanceType getInstanceType() {
		return instanceType;
	}
	
	public void setInstanceType(InstanceType type) {
		this.instanceType = type;
	}
	
	public boolean isTVTInstance() {
		return instanceType == InstanceType.TVT;
	}
	
	public boolean isFFAInstance() {
		return instanceType == InstanceType.FFA;
	}
	
	public boolean isBGInstance(){
        return instanceType == InstanceType.BG;
    }
	
	public boolean isPVPSoloInstance(){
        return instanceType == InstanceType.SOLO;
    }
	
	public boolean isPVPInstance() {
		return instanceType == InstanceType.PVP;
	}

}
